/*
 *  Copyright 2010-2011 Mark Eschbach
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 * $HeadURL: https://meschbach.com/svn/software/mee-utils/trunk/psi-jetty/src/main/java/com/meschbach/psi/jetty/JettyWebContainer.java $
 * $Id: JettyWebContainer.java 2793 2011-01-22 00:57:57Z meschbach $
 */
package com.meschbach.psi.jetty;

import com.meschbach.psi.WebContainer;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.mortbay.jetty.Server;
import org.mortbay.jetty.handler.AbstractHandler;
import org.mortbay.jetty.nio.SelectChannelConnector;
import org.mortbay.jetty.webapp.WebAppContext;

/**
 * A <code>JettyWebContainer</code> manages an instance of Jetty for usage by
 * a PSI client.  This includes managing the life cycle and will deploy an
 * instance of the <code>ContextManager</code> for remote deployment.<p>
 *
 * This class is copyright 2010-2011 by Mark Eschbach and is licensed under the
 * Apache License, Version 2.0; accessible at
 * http://www.apache.org/licenses/LICENSE-2.0.<p>
 * 
 * @author "Mark Eschbach" &lt;meschbach@gmail.com&gt;
 * @since 1.0.0
 * @version 1.3.1
 */
public class JettyWebContainer implements WebContainer<JettyWebContext> {

    /**
     * The <code>isRunning</code> is a flag to determine if the web container is
     * running.  When the flag is true, then the server is running.  Any access
     * to this variable should be synchronized against the containing instance.
     */
    protected boolean isRunning;
    /**
     * The <code>contexts</code> is a list of deployed web contexts.  Any access
     * to this variable should be synchronized against the containing instance.
     */
    protected List<JettyWebContext> contexts;
    /**
     * The <code>httpServer</code> is an instance of the Jetty service actually
     * receiving and processing the HTTP requests.
     */
    protected Server httpServer;
    /**
     * The <code>tcpipInput</code> is the socket controller for Jetty which
     * actually manages the low level details of accepting TCP/IP connections.
     * This is a non-blocking version of the Jetty connector.
     */
    protected SelectChannelConnector tcpipInput;

    /**
     * Constructs a new <code>JettyWebContainer</code> in a stopped state.
     */
    public JettyWebContainer() {
        isRunning = false;
        contexts = new LinkedList<JettyWebContext>();
    }

    /**
     * Full fills the requirements of the contract interface
     * @see WebContainer#createContext(java.lang.String, java.lang.String) 
     */
    public synchronized JettyWebContext createContext(String contextPath, String warPath) {
        /*
         * Attempt to find the file
         */
        File warFile = new File(warPath);
        if (!warFile.exists() && !warFile.isDirectory()) {
            throw new RuntimeException("War file '" + warFile.getAbsolutePath() + "' was not found");
        }
        /*
         * 
         */
        JettyWebContext jwc = new JettyWebContext(warFile, this, contextPath);
        return jwc;
    }

    /**
     * Full fills the requirements of the contract interface
     * @see WebContainer#getContexts() 
     */
    public synchronized List<JettyWebContext> getContexts() {
        return new ArrayList<JettyWebContext>(contexts);
    }

    /**
     * Full fills the requirements of the contract interface
     * @see WebContainer#stop() 
     */
    public synchronized void stop() {
        /*
         * Ensure we keep ourselves in a consistent state
         */
        if (!isRunning) {
            return;
        }
        /*
         * Shutdown the web contexts
         */
        Iterator<JettyWebContext> cit = contexts.iterator();
        while (cit.hasNext()) {
            try {
                cit.next().stop();
            } catch (Throwable t) {
                t.printStackTrace();
            }
        }
        contexts.clear();
        /*
         * Shutdown jetty
         */
        try {
            httpServer.setGracefulShutdown(1 * 1000);
            httpServer.stop();
        } catch (Throwable t) {
            t.printStackTrace();
        } finally {
            httpServer = null;
        }
        /*
         * Set our object state
         */
        isRunning = false;
    }

    /**
     * @see WebContainer#isRunning()
     * @return true if the web container is running
     */
    public synchronized boolean isRunning() {
        return isRunning;
    }

    /**
     * Retrieves the instance of the underlying Jetty server
     * @deprecated if you are using this you are doing something wrong
     * @return the underlying Jetty server
     */
    @Deprecated
    public synchronized Server getHttpServer() {
        return httpServer;
    }

    /**
     * Sets the under lying Jetty server
     * @param httpServer is the server to utilize.
     * @deprecated if you are using this you are doing something wrong
     */
    @Deprecated
    public synchronized void setHttpServer(Server httpServer) {
        this.httpServer = httpServer;
    }

    /**
     * Full fills the requirements of the contract interface.
     * @see WebContainer#start(int) 
     */
    public synchronized int start(int port) {
        /*
         * Gaurd against double entry
         */
        if (isRunning) {
            throw new IllegalStateException("Already running");
        }
        /*
         * Create our socket acceptor
         */
        tcpipInput = new SelectChannelConnector();
        tcpipInput.setPort(port);
        /*
         * Create our new Jetty Server
         */
        httpServer = new Server();
        httpServer.addConnector(tcpipInput);
        httpServer.setHandler(new HandlerMultiplexer(new ContextManager("/psi/", this)));
        try {
            httpServer.start();
        } catch (Exception re) {
            throw new IllegalStateException(re);
        }
        /*
         * Set our state
         */
        isRunning = true;
        /*
         * 
         */
        return tcpipInput.getLocalPort();
    }

    /**
     * Full fills the requirements of the contract interface
     * @see WebContainer#shutdown() 
     */
    public synchronized void shutdown() {
        /*
         * Ensure we are not running
         */
        stop();
    }

    /**
     * Creates a new instance of Jetty under the PSI container.  If a port is
     * specified under as the first parameter, then the service will attempt to
     * bind to that port.  If a port is not provided, then the service will
     * attempt to bind to port 0, allowing the operating system to choose the
     * port.
     * 
     * @param args an array containing the arguments, must be 0 or 1 [ port ]
     */
    public static void main(String args[]) {
        /*
         * Ensure we have a our argument count
         */
        int port;
        if (args.length != 1) {
            System.out.println("Warning: port not specified, starting on random port");
            port = 0;
        } else {
            port = Integer.parseInt(args[0]);
        }
        /*
         * Construct our service
         */
        try {
            JettyWebContainer jwc = new JettyWebContainer();
            int actualPort = jwc.start(port);
            System.out.println("Started on port " + actualPort);
        } catch (Throwable t) {
            t.printStackTrace();
        }
    }

    /**
     * Adds the given context to the available contexts to be deployed.
     * 
     * @param context is the context to be started and deployed
     */
    public void addContext(JettyWebContext context) {
        final List<JettyWebContext> theContexts = this.contexts;
        synchronized (theContexts) {
            theContexts.add(context);
        }
    }

    /**
     * Removes the given context from deployment.
     *
     * @param context is the context to be removed from deployment
     */
    public void removeContext(JettyWebContext context) {
        final List<JettyWebContext> theContexts = this.contexts;
        synchronized (theContexts) {
            theContexts.remove(context);
        }
    }

    /**
     * This class encapsulates the Jetty root handler, allowing us to dispatch
     * to the given contexts as they are deployed or undeployed.
     */
    protected class HandlerMultiplexer extends AbstractHandler {

        ContextManager cm;

        public HandlerMultiplexer(ContextManager cm) {
            this.cm = cm;
        }

        public void handle(String target, HttpServletRequest request, HttpServletResponse response, int dispatch) throws IOException, ServletException {
            /*
             * Attempt to handle via the context manager
             */
            cm.handle(target, request, response, dispatch);
            /*
             * If our context handler didn't dispatch, then
             */
            final List<JettyWebContext> theContexts = contexts;
            List<JettyWebContext> ctxs;

            synchronized (theContexts) {
                ctxs = new ArrayList<JettyWebContext>(contexts);
            }
            Iterator<JettyWebContext> wic = ctxs.iterator();
            while (!response.isCommitted() && wic.hasNext()) {
                JettyWebContext jwc = wic.next();
                synchronized (jwc) {
                    WebAppContext wac = jwc.getJettyContext();
                    if (!wac.isRunning()) {
                        System.out.println("Starting context " + wac.getContextPath() + " from WAR " + wac.getWar());
                        try {
                            wac.start();
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                    wac.handle(target, request, response, dispatch);
                }
            }
        }
    }
}
